#pragma once

#include <cmath>

#include "base.h"
#include "../impl/ma.h"
#include "../impl/s2.h"
#include "../impl/misc.h"

namespace mdtx::online::indicator
{
    template <typename T>
    using var = detail::indi_wrap_ro<updater::var<T>>;

    template <typename T>
    using var_vw = detail::indi_wrap_ro<updater::var_vw<T>>;

    template <typename T>
    using var_ew = detail::indi_wrap_cu<updater::var_ew<T>>;

    template <typename T>
    using var_cu = detail::indi_wrap_cu<updater::var<T>>;

    template <typename T>
    using var_cuvw = detail::indi_wrap_cu<updater::var_vw<T>>;

    template <typename T>
    class var_evw : public indi_weighted1<T>
    {
        using base = indi_weighted1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        boost::circular_buffer<T> buf;
        impl::sum<T> wsum{};
        impl::var_evw<T> var{};

    public:
        explicit var_evw(ptrdiff_t period)
            : buf(period)
        {
        }

        tuple_out update(tuple_in const &in) override
        {
            auto [p, v] = in;
            if (buf.full())
            {
                wsum.roll_impl(v, buf.front());
            }
            else
            {
                wsum.insert_impl(v);
            }
            buf.push_back(v);

            auto w = v / wsum.s;
            var.insert_impl(p, w);

            return tuple_out(var.s2);
        }
    };

    template <typename T>
    class volatility : public indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        boost::circular_buffer<T> buf;
        impl::var<T> var{};
        impl::logret<T> ret{};
        T ann;

    public:
        volatility(ptrdiff_t period, ptrdiff_t annualise)
            : buf(period),
              ann(std::sqrt(annualise))
        {
        }

        /**
         * @brief update
         *
         * @param in price
         * @return tuple_out
         */
        tuple_out update(tuple_in const &in) override
        {
            T x = std::get<0>(in);

            if (ret.init)
            {
                ret.insert_impl(x);
                var.insert_impl(ret.ret);
                if (buf.full())
                {
                    var.remove_impl(buf.front());
                }
                buf.push_back(ret.ret);
            }
            else
            {
                ret.insert_impl(x);
            }

            T sigma{};
            if (var.n > 1 && var.s2 > 0)
            {
                sigma = std::sqrt(var.s2 / (var.n - 1));
            }
            return tuple_out(sigma);
        }
    };

    template <typename T>
    class volatility_cu : public indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        impl::var<T> var{};
        impl::logret<T> ret{};
        T ann;

    public:
        explicit volatility_cu(ptrdiff_t annualise)
            : ann(std::sqrt(annualise))
        {
        }

        /**
         * @brief update
         *
         * @param in price
         * @return tuple_out
         */
        tuple_out update(tuple_in const &in) override
        {
            T x = std::get<0>(in);

            if (ret.init)
            {
                ret.insert_impl(x);
                var.insert_impl(ret.ret);
            }
            else
            {
                ret.insert_impl(x);
            }

            T sigma{};
            if (var.n > 1 && var.s2 > 0)
            {
                sigma = std::sqrt(var.s2 / (var.n - 1));
            }
            return tuple_out(sigma);
        }
    };

    template <typename T>
    class volatility_ew : public indi_unary1<T>
    {
        using base = indi_unary1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        impl::var_ew<T> var{};
        impl::logret<T> ret{};
        T ann;

    public:
        volatility_ew(ptrdiff_t period, ptrdiff_t annualise)
            : var(T(2) / (period + 1)),
              ann(std::sqrt(annualise))
        {
        }

        /**
         * @brief update
         *
         * @param in price
         * @return tuple_out
         */
        tuple_out update(tuple_in const &in) override
        {
            T x = std::get<0>(in);

            if (ret.init)
            {
                ret.insert_impl(x);
                var.insert_impl(ret.ret);
            }
            else
            {
                ret.insert_impl(x);
            }

            T sigma{};
            if (var.s2 > 0)
            {
                sigma = std::sqrt(var.s2);
            }
            return tuple_out(sigma);
        }
    };

    /**
     * @brief Volume weighted annualised volatility
     *
     * @details Because volume weighted, linear return is used instead of log return.
     *
     * @tparam T
     */
    template <typename T>
    class volatility_vw : public indi_weighted1<T>
    {
        using base = indi_weighted1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        boost::circular_buffer<T> buf;
        impl::var_vw<T> var{};
        impl::linret<T> ret{};
        T ann;

    public:
        volatility_vw(ptrdiff_t period, ptrdiff_t annualise)
            : buf(period * 2),
              ann(std::sqrt(annualise))
        {
        }

        /**
         * @brief update
         *
         * @param in price, weight
         * @return tuple_out
         */
        tuple_out update(tuple_in const &in) override
        {
            auto [x, w] = in;

            if (ret.init)
            {
                ret.insert_impl(x);
                var.insert_impl(ret.ret, w);
                if (buf.full())
                {
                    var.remove_impl(buf[0], buf[1]);
                }
                bpush(buf, ret.ret, w);
            }
            else
            {
                ret.insert_impl(x);
            }

            T sigma{};
            T w_adj = impl::rel_weight(var.wsum, var.w2sum);
            if (var.s2 > 0 && w_adj > 0)
            {
                sigma = std::sqrt(var.s2 / w_adj);
            }
            return tuple_out(sigma);
        }
    };

    template <typename T>
    class volatility_cuvw : public indi_weighted1<T>
    {
        using base = indi_weighted1<T>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        impl::var_vw<T> var{};
        impl::linret<T> ret{};
        T ann;

    public:
        explicit volatility_cuvw(ptrdiff_t annualise)
            : ann(std::sqrt(annualise))
        {
        }

        /**
         * @brief update
         *
         * @param in price, weight
         * @return tuple_out
         */
        tuple_out update(tuple_in const &in) override
        {
            auto [x, w] = in;

            if (ret.init)
            {
                ret.insert_impl(x);
                var.insert_impl(ret.ret, w);
            }
            else
            {
                ret.insert_impl(x);
            }

            T sigma{};
            T w_adj = impl::rel_weight(var.wsum, var.w2sum);
            if (var.s2 > 0 && w_adj > 0)
            {
                sigma = std::sqrt(var.s2 / w_adj);
            }
            return tuple_out(sigma);
        }
    };
}
